using System;
using System.IO;

namespace Game.Lib
{
    public class Vector3 : Vector
    {
        #region Constants
        
        public readonly static Vector3 Zero = new Vector3();
        public readonly static Vector3 XAxis = new Vector3(1f, 0f, 0f);
        public readonly static Vector3 YAxis = new Vector3(0f, 1f, 0f);
        public readonly static Vector3 ZAxis = new Vector3(0f, 0f, 1f);
        public readonly static Vector3 NegXAxis = new Vector3(-1f, 0f, 0f);
        public readonly static Vector3 NegYAxis = new Vector3(0f, -1f, 0f);
        public readonly static Vector3 NegZAxis = new Vector3(0f, 0f, -1f);
        public readonly static Vector3 AllOne = new Vector3(1f, 1f, 1f);
        public readonly static Vector3 NegAllOne = new Vector3(-1f, -1f, -1f);
        
        #endregion
        
        #region Fields
        
        public float X;
        public float Y;
        public float Z;
        
        #endregion
        
        #region Constructors
        
        public Vector3()
        {
            X = Y = Z = 0f;
        }
        
        public Vector3(float s)
        {
            X = Y = Z = s;
        }
        
        public Vector3(float x, float y, float z)
        {
            X = x;
            Y = y;
            Z = z;
        }
        
        public Vector3(Vector2 a, float z)
        {
            X = a.X;
            Y = a.Y;
            Z = z;
        }
        
        public Vector3(Vector3 a)
        {
            X = a.X;
            Y = a.Y;
            Z = a.Z;
        }
        
        public Vector3(Vector3d a)
        {
            X = (float)a.X;
            Y = (float)a.Y;
            Z = (float)a.Z;
        }
        
        #endregion
        
        #region Methods
        
        public void Normalize()
        {
            float mag = Magnitude();
            if (mag > 0.000001f)
            {
                mag = 1f / mag;
                X *= mag;
                Y *= mag;
                Z *= mag;
            }
            else
            {
                X = Y = Z = 0f;
            }
        }
        
        public float Magnitude()
        {
            return (float)Math.Sqrt(X * X + Y * Y + Z * Z);
        }
        
        public float MagnitudeSquared()
        {
            return X * X + Y * Y + Z * Z;
        }
        
        public override int GetHashCode()
        {
            return X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode();
        }
        
        public override string ToString()
        {
            return String.Format(Utils.EnUsCulture, "<{0}, {1}, {2}>", X, Y, Z);
        }

        public override bool Equals(object obj)
        {
            if(!(obj is Vector3)) return false;
            return this == (Vector3)obj;
        }
        
        #endregion
        
        #region Static Methods
        
        public static Vector3 Abs(Vector3 a)
        {
            Vector3 ret = new Vector3(a);
            if(ret.X < 0f) ret.X = -ret.X;
            if(ret.Y < 0f) ret.Y = -ret.Y;
            if(ret.Z < 0f) ret.Z = -ret.Z;
            return ret;
        }
        
        public static Vector3 Lerp(Vector3 a, Vector3 b, float t)
        {
            return a + (b - a) * t;
        }
        
        public static bool ApproxEqual(Vector3 a, Vector3 b, float err)
        {
            return (float)Math.Abs(a.X - b.X) <= err &&
                   (float)Math.Abs(a.Y - b.Y) <= err &&
                   (float)Math.Abs(a.Z - b.Z) <= err;
        }
        
        public static bool IsFinite(Vector3 a)
        {
            return  Utils.IsFinite(a.X) &&
                    Utils.IsFinite(a.Y) &&
                    Utils.IsFinite(a.Z);
        }
        
        public static Vector3 Clamp(Vector3 test, Vector3 min, Vector3 max)
        {
            return new Vector3(
			            Utils.Clamp(test.X, min.X, max.X),
			            Utils.Clamp(test.Y, min.Y, max.Y), 
			            Utils.Clamp(test.Z, min.Z, max.Z));
        }
        
        public static float Distance(Vector3 a, Vector3 b)
        {
            return (a - b).Magnitude();
        }
        
        public static float DistanceSquared(Vector3 a, Vector3 b)
        {
            return (a - b).MagnitudeSquared();
        }
        
        public static Vector3 Min(Vector3 a, Vector3 b)
        {
            a.X = Utils.Min(a.X, b.X);
            a.Y = Utils.Min(a.Y, b.Y);
            a.Z = Utils.Min(a.Z, b.Z);
            return a;
        }
        
        public static Vector3 Max(Vector3 a, Vector3 b)
        {
            a.X = Utils.Max(a.X, b.X);
            a.Y = Utils.Max(a.Y, b.Y);
            a.Z = Utils.Max(a.Z, b.Z);
            return a;
        }
        
        public static Vector3 Snap(Vector3 a, float t)
        {
            a.X = Utils.Snap(a.X, t);
            a.Y = Utils.Snap(a.Y, t);
            a.Z = Utils.Snap(a.Z, t);
            return a;
        }
        
        public static Vector3 Parse(string val)
        {
            char[] splitChar = { ',' };
            string[] split = val.Replace("<", String.Empty).Replace(">", String.Empty).Split(splitChar);
            if(split.Length == 3)
            {
	            return new Vector3(
	                Single.Parse(split[0].Trim(), Utils.EnUsCulture),
	                Single.Parse(split[1].Trim(), Utils.EnUsCulture),
	                Single.Parse(split[2].Trim(), Utils.EnUsCulture));
            }
            else throw new ArgumentException("Vectors must be of the form \"<X, Y, Z>\"");
        }
        
        public static Quaternion RotationBetween(Vector3 a, Vector3 b)
        {
            float dotProduct = a * b;
            float magProduct = a.Magnitude() * b.Magnitude();
            float angle = (float)Math.Acos(dotProduct / magProduct);
            Vector3 axis = a % b;
            axis.Normalize();
            return new Quaternion(axis, angle);
        }
        
        #endregion
        
        #region Operators
        
        public static Vector3 operator +(Vector3 a, Vector3 b)
        {
            return new Vector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
        }
        
        public static Vector3 operator -(Vector3 a, Vector3 b)
        {
            return new Vector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
        }
        
        public static Vector3 operator -(Vector3 a)
        {
            return new Vector3(-a.X, -a.Y, -a.Z); ;
        }
        
        public static Vector3 operator *(float s, Vector3 a)
        {
            return new Vector3(s * a.X, s * a.Y, s * a.Z);
        }
        
        public static Vector3 operator *(Vector3 a, float s)
        {
            return new Vector3(s * a.X, s * a.Y, s * a.Z);
        }
        
        public static float operator *(Vector3 a, Vector3 b)
        {
            return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
        }
        
        public static Vector3 operator *(Vector3 vec, Quaternion rot)
        {
            float rw = -rot.X * vec.X - rot.Y * vec.Y - rot.Z * vec.Z;
            float rx = rot.W * vec.X + rot.Y * vec.Z - rot.Z * vec.Y;
            float ry = rot.W * vec.Y + rot.Z * vec.X - rot.X * vec.Z;
            float rz = rot.W * vec.Z + rot.X * vec.Y - rot.Y * vec.X;

            Vector3 a = new Vector3();

            a.X = -rw * rot.X + rx * rot.W - ry * rot.Z + rz * rot.Y;
            a.Y = -rw * rot.Y + ry * rot.W - rz * rot.X + rx * rot.Z;
            a.Z = -rw * rot.Z + rz * rot.W - rx * rot.Y + ry * rot.X;

            return a;
        }
        
        public static Vector3 operator /(Vector3 vec, Quaternion r)
        {
            Quaternion rot = ~r;
            float rw = -rot.X * vec.X - rot.Y * vec.Y - rot.Z * vec.Z;
            float rx = rot.W * vec.X + rot.Y * vec.Z - rot.Z * vec.Y;
            float ry = rot.W * vec.Y + rot.Z * vec.X - rot.X * vec.Z;
            float rz = rot.W * vec.Z + rot.X * vec.Y - rot.Y * vec.X;

            Vector3 a = new Vector3();

            a.X = -rw * rot.X + rx * rot.W - ry * rot.Z + rz * rot.Y;
            a.Y = -rw * rot.Y + ry * rot.W - rz * rot.X + rx * rot.Z;
            a.Z = -rw * rot.Z + rz * rot.W - rx * rot.Y + ry * rot.X;

            return a;
        }
        
        
        public static Vector3 operator /(Vector3 a, float s)
        {
            return a * (1f / s);
        }
        
        public static Vector3 operator %(Vector3 a, Vector3 b)
        {
            return new Vector3(
                a.Y * b.Z - b.Y * a.Z,
                a.Z * b.X - b.Z * a.X,
                a.X * b.Y - b.X * a.Y);
        }
        
        public static bool operator ==(Vector3 a, Vector3 b)
        {
            return  a.X == b.X &&
                    a.Y == b.Y &&
                    a.Z == b.Z;
        }
        
        public static bool operator !=(Vector3 a, Vector3 b)
        {
            return  a.X != b.X ||
                    a.Y != b.Y ||
                    a.Z != b.Z;
        }
        
        #endregion
        
        #region Serialization

        public void FromBytes(byte[] data, int pos)
        {
            if(!BitConverter.IsLittleEndian)
            {
                Array.Reverse(data, pos, 4);
                Array.Reverse(data, pos + 4, 4);
                Array.Reverse(data, pos + 8, 4);
            }
            X = BitConverter.ToSingle(data, pos);
            Y = BitConverter.ToSingle(data, pos + 4);
            Z = BitConverter.ToSingle(data, pos + 8);
        }
        
        public byte[] ToBytes()
        {
            byte[] dest = new byte[12];
            Buffer.BlockCopy(BitConverter.GetBytes(X), 0, dest, 0, 4);
            Buffer.BlockCopy(BitConverter.GetBytes(Y), 0, dest, 4, 4);
            Buffer.BlockCopy(BitConverter.GetBytes(Z), 0, dest, 8, 4);
            if(!BitConverter.IsLittleEndian)
            {
                Array.Reverse(dest, 0, 4);
                Array.Reverse(dest, 4, 4);
                Array.Reverse(dest, 8, 4);
            }
            return dest;
        }
        
        public void Serialize(Stream s)
        {
            if(!s.CanWrite) throw new ArgumentException("Serialization requires a writable stream");
            s.Write(ToBytes(), 0, 12);
        }
        
        public void Deserialize(Stream s)
        {
            if(!s.CanRead) throw new ArgumentException("Serialization requires a writable stream");
            byte[] buf = new byte[12];
            if(s.Read(buf, 0, 12) != 12) throw new EndOfStreamException("Quaternions require at least 16 bytes of data");
            if(!BitConverter.IsLittleEndian)
            {
                Array.Reverse(buf, 0, 4);
                Array.Reverse(buf, 4, 4);
                Array.Reverse(buf, 8, 4);
            }
            X = BitConverter.ToSingle(buf, 0);
            Y = BitConverter.ToSingle(buf, 4);
            Z = BitConverter.ToSingle(buf, 8);
        }

        #endregion
    }
}
